security: add gitleaks allowlist — greens the now-real secret gate on main#512
Conversation
Follow-up to #511. That PR merged at its first commit, so the caller fix landed without this allowlist and main's Secret Scanner went straight from startup_failure (dead) to failure (real gate, 15 false positives). This completes the change. Net effect of #511 + this commit: the scanner goes from not executing at all to executing and passing honestly. All 15 findings were verified individually as false positives: * deno.lock x8 — SHA-256 integrity hashes of public registry modules * Project.toml — a Julia General-registry package UUID * otpiser audit — a 40-hex OpenPGP fingerprint (published by design) * haskell design md — the type name in `data Ed25519KeyPair = Ed25519KeyPair` * example.zig — `abc123def456...`, commented "// Would be real token" * NEXT-STEPS.md x2 — `1234567890_abc123...` in a fenced terminal mockup * examples.ndjson — `0a1b2c3d4e5f...`, an ascending-nibble filler walk This wall-of-noise is almost certainly what the original `continue-on-error: true` was papering over. So the allowlist is the *unblocker* for re-pinning callers onto the real gate estate-wide, not merely local cleanup. Design rule, documented in the file: allowlist by pattern class, anchored to the whole value; never allowlist a path because "that file is fine" and never disable a rule. `paths` is confined to formats that cannot hold a live credential (lockfile integrity hashes, dependency UUIDs). Every regex is anchored ^...$ deliberately. An earlier draft used unanchored `(?i)test|example`, and canary-testing caught that it silently allowlisted the real AWS key wJalrXUtnFEMI/K7MDENG/bPxRfiCY... purely because "EXAMPLE" appears inside it. Anchoring fixed it. Verified against origin/main: scan -> no leaks found (exit 0) planted realistic secrets -> still detected: github-pat, gitlab-pat, slack-bot-token, stripe-access-token, generic-api-key (AWS) Also clears all 8 of boj-server-cartridges' equivalent fixture findings with no repo-specific additions (see that repo's PR #99), which is the evidence it stands up as the estate baseline template rather than a standards-only patch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hypatia flagged .gitleaks.toml itself — "Secret found: GitLab PAT" — because the comment documenting the repeated-character mask contained a literal masked token. The allowlist file tripping the estate's own secret detector is a neat demonstration of the problem it exists to solve. Describe the shape in prose instead, and record the rule in the file so the next editor does not reintroduce a literal example token. Behaviour is unchanged: the regex is untouched. Re-verified — standards scans clean (exit 0) and planted github-pat / gitlab-pat / slack-bot-token / stripe-access-token values are all still detected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Independent verification — this config holds up, and the anchoring warning is not theoretical. I built a My draft had the exact hole you describe. I used
A live GitHub PAT silently allowlisted because the line mentioned "EXAMPLE". Matching the line instead of the value is the root error; your Your config, same harness:
So it clears the 15 without weakening detection on any case I could construct. Deferring to this PR; no competing file from me. One note for whoever re-pins callers afterwards: Context: #514 fixes the two remaining |
…this repo only) + scorecard-verify false-red (new) (#514) Completes the run to green on `main`, after #511 (secret-scanner caller + SPDX) and #512 (gitleaks allowlist). Two remaining causes, independent of each other and of those PRs. ## 1. Hypatia has never run — the caller under-grants (#451) `hypatia-scan-reusable.yml` declares `security-events: write` (it uploads SARIF). `hypatia-scan.yml` granted `security-events: **read**`. A called workflow may never request more than its caller grants. When it does, GitHub rejects the run at **startup** — before any job exists: ``` completed startup_failure Hypatia Security Scan main push 1s $ gh run view 29764318317 --log-failed failed to get run log: log not found # <- there is no log; no job ran ``` Granting `read` did not degrade the scan to a no-upload mode. **It stopped the scan from existing.** The reusable carried this comment: ```yaml continue-on-error: true # callers granting only security-events:read skip gracefully ``` That is a false mental model, and it is load-bearing — it is the reason the mismatch looked intentional. Nothing skips gracefully in a job that was never created. Corrected in place. This is exactly the #451 pattern that startup-fails reusable-workflow callers estate-wide. **Fixed here on the canon's own caller only** — deliberately demonstrated on one repo before any propagation is proposed. ## 2. Seven "fake passes" that were every one of them real (#381) `registry-verify.yml` runs `build-scorecards.sh --verify`, which **executes** every pass-row's check. The job installed no tools, and `ubuntu-latest` ships neither ripgrep nor xmllint: | rows | mechanism | observed | |---|---|---| | `k9-svc/M6` | calls `xmllint` directly | exit **127** | | 6 × `release-pre-flight/*` | invoke `v1-audit.sh`, which hard-exits 2 without ripgrep, so the greps match nothing | exit **1** | The verifier reported all seven as **"claimed PASS but the pass is not real"**. Reproduced locally by stripping `rg` from `PATH` — identical output. Measured with the tools present: ``` ── verify: 87 grounded pass · 0 broken pass · 0 unrunnable · 0 self-asserted · 0 stale-fail EXIT=0 ``` **All seven claims were true.** The checks had never run. A false-red is not harmless: it trains maintainers to ignore a gate — the same end state as the fake-green gates #500 removed, reached from the opposite direction. Two changes, since installing the tools alone leaves the trap armed for the next missing dependency: - `registry-verify.yml` installs `ripgrep` + `libxml2-utils`. - `run_verify()` **refuses to judge pass-rows at all** in an incomplete environment, and reports genuinely unrunnable checks as `unrunnable` — still failing, since an unverifiable pass must never go green — rather than as false claims. ### The required-tool list is declared, not inferred An earlier draft parsed check strings to discover their commands. It misparsed quoted regexes (`"a\|b\|c"`) and `[ $(…) -ge 25 ]`, inventing **35** failures — precisely the sin the guard exists to prevent. Replaced with a measured, documented list: `xmllint` is named by 4 checks, `jq` by 1, and `rg` by none but is required transitively via `v1-audit.sh`. ## Verification - complete env → `87 grounded · 0 broken · 0 unrunnable`, exit 0 - incomplete env → fails fast, judges nothing, names the missing tool - `k9-svc/mime/k9.xml` + `k9.uti.plist` independently confirmed well-formed and the two companion files present — so installing xmllint turns that row **genuinely** green rather than exposing a real fault - all three workflow files parse ## Not included, on purpose `.gitleaks.toml` — **#512 covers it and is the better implementation.** I built one independently and then discarded it: my regexes matched against the whole line unanchored, and canary-testing showed that suppressed real tokens. | planted secret | gitleaks defaults | my draft | |---|---|---| | `ghp_…` PAT | caught | caught | | same PAT, line also contains `EXAMPLE_KEY` | caught | **suppressed** | | same PAT, line ends `# ...` | caught | **suppressed** | #512's anchored `^…$` design has no such hole. Verified independently against gitleaks 8.18.4 (the exact CI build, checksum-matched): all four adversarial fixtures still caught, and `0` findings on the tree. Adopt #512. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
…518) Found while verifying the 201-repo secret-scanner re-pin sweep. Two defects in `rust-secrets`. ## 1. Silent no-scan — the serious one ```bash grep -rn --include="*.rs" -E "$pattern" src/ # <- root src/ only ``` A Cargo **workspace** keeps its code in `crates/*/src`, so `src/` does not exist at the root. `grep` exits **2**, the enclosing `if` reads that as *no match*, and the job passes. **Measured across the 64 Rust repos in the sweep: 8 (12.5%) have no root `src/` and were never scanned at all.** Proven on a fixture — a planted secret in `crates/core/src/lib.rs`: | | old job | this PR | |---|---|---| | `pub const SERVICE_TOKEN: &str = "…"` in `crates/core/src/lib.rs` | **exit 0** ✅ green | detected | Another gate that could not fail. ## 2. False positive on the correct pattern `let.*api_key.*=.*"` matches any `let api_key =` line containing a quote — including: ```rust let api_key = env::var("CLOUDGUARD_API_KEY").ok(); ``` …which is *precisely* the practice this job exists to enforce. This was the **only** `rust-secrets` failure in the entire sweep (`cloudguard-server` `src/main.rs:59`). A gate whose sole route to green is to stop reading from the environment is inverted. ## Exemptions Mirror the four layers already documented on `shell-secrets`, and match on the **value** or an explicit pragma — **never on a file path**, because a path exemption blinds the job to a real secret in the same file (the failure mode `.gitleaks.toml` was rewritten to avoid in #512). | Layer | Rationale | |---|---| | env-lookup RHS | the correct pattern. Anchored to the assignment and stopped at `;`, so a literal secret with an unrelated `env::var` mention later on the line **still trips** | | URL values | an OAuth endpoint is public, not a credential (`const MS_TOKEN_URL = "https://login.microsoftonline.com/…"` tripped `const.*TOKEN.*=.*"` 4× in one repo) | | comment lines | documentation of a pattern is not a live secret | | `// scanner-allow: rust-secrets` | explicit, per-line, auditable | ## Warn-first on the widened scope Hits **outside `./src`** are newly visible, so they warn until **2026-08-21** — the same cutoff and idiom as `check-package-policy.sh` / `check-docs-presence.sh` — rather than reddening repos that were never actually being scanned. Hits **inside `./src`** block exactly as before, so there is no regression. The advisory branch prints the finding and says `NOT YET ENFORCED`; it never claims a pass. A malformed cutoff **refuses to run** rather than defaulting to warn forever. ## Verified, not assumed Canary suite run against the script **extracted from this YAML** (`yaml.safe_load` → execute), not a copy: ``` PASS env::var + comment + pragma + OAuth URL -> ALLOWED PASS secret in ./src -> BLOCKS PASS secret in crates/ -> ADVISORY today PASS secret in crates/ -> BLOCKS after cutoff PASS literal + env mention after ; -> BLOCKS (exemption not over-broad) PASS malformed cutoff -> refuses to run 6 passed, 0 failed ``` Replayed over **all 64 Rust repos** in the sweep: **0 regressions today, 1 fixed** (`cloudguard-server`). ## Follow-up for repo owners (before 2026-08-21) Three repos carry fixture-shaped hits outside `./src` and need a one-line pragma; each is named in the run output today: - `echidnabot` — `fuzz/fuzz_targets/fuzz_hmac.rs:17` (fuzz corpus) - `filesoup` — `plugins/secret-scanner/src/lib.rs:159` (the plugin's own detection fixture) - `heterogenous-mobile-computing` — `examples/basic_usage.rs:61` (demo of a *blocked* query) Pre-existing and unchanged by this PR: `aerie` and `conative-gating` fail under both old and new (`request["api_key"]` dynamic lookup; a scanner's own `r#"…"#` test fixtures). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>



Follow-up to #511 —
mainis currently red because of it.#511 merged at its first commit, so the caller fix landed without this allowlist.
main's Secret Scanner therefore went straight fromstartup_failure(dead — zero jobs, since 2026-07-06) tofailure(real gate, 15 false positives). This completes the change.Net effect of #511 + this PR: the scanner goes from not executing at all, to executing and passing honestly.
The 15 findings — all verified false positives
deno.lock×8lol/analysis/Project.tomldocs/audits/otpiser-…a2mlrhodium…/haskell-registry-design.mdEd25519KeyPairavow-lib/…/example.zigabc123def456...// Would be real tokentelegram-bot/NEXT-STEPS.md×21234567890_abc123...…/examples.ndjson0a1b2c3d4e5f…This wall of noise is almost certainly what the original
continue-on-error: truewas papering over — turn the gate on with no allowlist, get 15 reds that are entirely noise, someone reverts, and the estate is back to a scanner that cannot fail. So this allowlist is the unblocker for re-pinning callers estate-wide, not merely local cleanup.Design rule (documented in the file — please keep it)
Allowlist by pattern class, anchored to the whole value. Never allowlist a path because "that file is fine", never disable a rule.
pathsis confined to formats that cannot hold a live credential (lockfile integrity hashes, dependency UUIDs).Every regex is anchored
^...$deliberately. An earlier draft used unanchored(?i)test|exampleand canary-testing caught that it silently allowlisted the real AWS keywJalrXUtnFEMI/K7MDENG/bPxRfiCY…purely because "EXAMPLE" appears inside it. Anchoring fixed it.Verification against
origin/mainwith planted realistic secrets still detected:
github-pat,gitlab-pat,slack-bot-token,stripe-access-token,generic-api-key(AWS).Also clears all 8 of
boj-server-cartridges' equivalent fixture findings with no repo-specific additions (see boj-server-cartridges#99) — the evidence it works as the estate baseline template, not a standards-only patch.🤖 Generated with Claude Code